package com.onlygx.dva.redis.annotation.repeat;

import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONUtil;
import com.onlygx.dva.redis.constant.CacheConstant;
import com.onlygx.dva.redis.constant.RedisKeyConstant;
import com.onlygx.dva.redis.exception.RepeatRequestException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 * 允许并发控制访问控制台程序的请求次数。
 * </p>
 *
 * @author 高祥
 * @since 2023-03-13
 */

@Slf4j
@Component
@Aspect
@RequiredArgsConstructor
public class NoRepeatSubmitAspectHandler {

    private final RedisTemplate<String, String> redisTemplate;

    /**
     * 定义切点
     */
    @Pointcut("@annotation(com.onlygx.dva.redis.annotation.repeat.CoreRepeatSubmit)")
    public void preventDuplication() {}

    @Around("preventDuplication()")
    public Object around(ProceedingJoinPoint joinPoint) throws Exception {

        //获取请求信息
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();

        HttpServletRequest request = null;
        if (attributes != null) {
            request = attributes.getRequest();
        }else{
            // 重复提交了抛出异常，如果是在项目中，根据具体情况处理。
            throw new RepeatRequestException("验证重复请求失败，未获取到请求信息。");
        }

        // 获取执行方法
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();

        //获取防重复提交注解
        CoreRepeatSubmit annotation = method.getAnnotation(CoreRepeatSubmit.class);

        // 是否获取token以及方法标记
        String token = "notIncludeUser";
        if(Boolean.TRUE.equals(annotation.includeUser())){
            token = request.getHeader(CacheConstant.CURRENT_USER);
        }

        //获取url
        String url = request.getRequestURI();

        //通过前缀 + url + token + 函数参数签名 来生成redis上的 key
        String redisKey = RedisKeyConstant.METHOD_REPEAT_SUBMIT
                .concat(DigestUtil.sha1Hex(url)).concat(":")
                .concat(DigestUtil.sha1Hex(token)).concat(":")
                .concat(getMethodSign(method, joinPoint.getArgs()));

        // 这个值只是为了标记，不重要
        String redisValue = redisKey.concat(":")
                .concat(annotation.value()).concat(":")
                .concat("submit duplication");

        //log.info("生成防止重复提交的key：{}，value:{}",redisKey,redisValue);
        if (Boolean.FALSE.equals(redisTemplate.hasKey(redisKey))) {
            try {
                // 设置防重复操作限时标记(前置通知)
                boolean isAbsent = Boolean.TRUE.equals(redisTemplate.opsForValue()
                        .setIfAbsent(redisKey, redisValue, annotation.expireSeconds(), TimeUnit.SECONDS));
                if(!isAbsent){
                    log.info("已存在的请求，触发重复提交。");
                    throw new RepeatRequestException("请勿重复提交");
                }
                //正常执行方法并返回
                //ProceedingJoinPoint类型参数可以决定是否执行目标方法，
                // 且环绕通知必须要有返回值，返回值即为目标方法的返回值
                return joinPoint.proceed();
            } catch (Throwable throwable) {
                //确保方法执行异常实时释放限时标记(异常后置通知)
                redisTemplate.delete(redisKey);
                throw new RuntimeException(throwable);
            }
        } else {
            // 重复提交了抛出异常，如果是在项目中，根据具体情况处理。
            throw new RepeatRequestException("请勿重复提交");
        }
    }

    /**
     * 生成方法标记：采用数字签名算法SHA1对方法签名字符串加签
     *
     * @param method
     * @param args
     * @return
     */
    private String getMethodSign(Method method, Object... args) {
        StringBuilder sb = new StringBuilder(method.toString());
        for (Object arg : args) {
            sb.append(toString(arg));
        }
        return DigestUtil.sha1Hex(sb.toString());
    }

    private String toString(Object arg) {
        if (Objects.isNull(arg)) {
            return "null";
        }
        if (arg instanceof Number) {
            return arg.toString();
        }
        return JSONUtil.toJsonStr(arg);
    }

}
